package cn.b504.qpcr.impls.cal;

import Jama.Matrix;
import cn.b504.qpcr.entity.Coefficient;
import cn.b504.qpcr.entity.PlateSetting;
import cn.b504.qpcr.entity.Well;
import cn.b504.qpcr.interfaces.cal.Calibrate;
import cn.b504.qpcr.utils.MathUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Future;

@Service
public class CalibrateImpl implements Calibrate {

  @Override
  @Async
  public Future<?> calibrate(Well well, List<Coefficient> coefficients) {
    List<PlateSetting.Target> targets = well.getTargets();
    // 当前孔设置的 targets 数少于两个，则不校准
    if (targets.size() < 2) {
      return new AsyncResult<>(null);
    }
    // 第一步：因为每个孔需要单独校准，所以每次遍历都要从原始系数矩阵开始入手
    final List<Coefficient> allCoefficientsList = new ArrayList<>(coefficients.size());
    coefficients.forEach(coe -> allCoefficientsList.add(coe.copy()));
    // 第二步：根据 channel 名字取出来的系数矩阵: n x 6
    List<Double[]> coefficientList = getCoefficientList(targets, allCoefficientsList);
    // 第三步：调序。核心数据：coeList、coeIndex、flag
    List<Double[]> coeList = new ArrayList<>(); // 记录 coefficientList 顺序调整正确后的结果
    List<Integer> coeIndex = new ArrayList<>(); // 记录需要校准的数据对应 channel 位置，0-based
    final boolean[] flag = new boolean[]{false, false, false, false, false, false}; // 每个元素表示当前位置的 channel 是否需要校准
    for (int i = 0; i < flag.length; i++) {
      final int index = i;
      Optional<Double[]> coeOptional = coefficientList.stream().filter(c -> c[index] == 1).findAny(); // 找这个系数：1x6
      if (coeOptional.isPresent()) {
        coeList.add(coeOptional.get());
        coeIndex.add(i);
        flag[i] = true;
      }
    }
    if (coeIndex.size() < 2) {
      return new AsyncResult<>(null);
    }
    // 第四步：删除多余的列，形成方阵。直接 set 进 coeList 里。
    trimCol(coeList, flag);
    // 第五步：取出要校准的数据
    final List<List<Float>> _data = prepareData(coeIndex, well.getLines());
    if (_data == null) {
      return new AsyncResult<>(null);
    }
    // 第六步：校准核心操作
    double[][] coeArr = MathUtils.wrapper2PrimitiveListDoubleArray(coeList);
    coreCalibrate(coeArr, _data);
    return new AsyncResult<>(null);
  }

  // 根据 channel 名字取出来的准确的系数矩阵
  private List<Double[]> getCoefficientList(List<PlateSetting.Target> targets, final List<Coefficient> allCoefficientsList) {
    final List<Double[]> coefficientList = new ArrayList<>();
    targets.forEach(target -> {
      Optional<Coefficient> coeOptional = allCoefficientsList.stream().filter(coe -> coe.getName().equals(target.getChannel())).findAny();
      if (!coeOptional.isPresent()) {
        // 没有对应系数, 则模拟
        Double[] arr = new Double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
        arr[target.getChannelIndex()] = 1.0;
        coefficientList.add(arr);
        return;
      }
      Double[] coefficient = coeOptional.get().getCoefficient();
      // 系数不全, 则直接跳过
      if (coefficient.length < 6) {
        return;
      }
      coefficientList.add(coefficient);
    });
    return coefficientList;
  }

  /**
   * 依据 flag 的内容对 list 进行修剪，直接将修剪后的结果扔进 list
   *
   * @param list 被修剪的 list
   * @param flag 依据
   */
  private void trimCol(List<Double[]> list, boolean[] flag) {
    for (int i = 0; i < list.size(); i++) {
      Double[] doubles = list.get(i);
      // 将不删的存到 newList 里，最后再 toArray 替换掉原来的数据
      List<Double> newList = new ArrayList<>();
      for (int j = 0; j < flag.length; j++) {
        if (flag[j]) {
          newList.add(doubles[j]);
        }
      }
      list.set(i, newList.toArray(new Double[0]));
    }
  }

  // 取出要校准的数据
  private List<List<Float>> prepareData(List<Integer> coeIndex, final Well.Line[] lines) {
    final List<List<Float>> data = new ArrayList<>();
    // 判断是否存在空数据
    if (coeIndex.stream().anyMatch(ci -> {
      List<Float> channelData = lines[ci].getQuantificationOpt();
      data.add(channelData);
      return channelData.size() == 0;
    })) {
      return null;
    }
    return data;
  }

  private void coreCalibrate(double[][] coeArr, List<List<Float>> _data) {
    // 数据可能参差不齐，取所有数据中的最小范围进行校准
    int minLength = Integer.MAX_VALUE;
    for (List<?> d : _data) {
      minLength = Math.min(minLength, d.size());
    }
    // 正式校准
    Matrix matrixInverse = new Matrix(coeArr).transpose().inverse(); // 逆矩阵
    for (int i = 0; i < minLength; i++) {
      double[][] B = new double[_data.size()][1];
      for (int j = 0; j < _data.size(); j++) {
        B[j][0] = (double) _data.get(j).get(i);
      }
      double[][] A = matrixInverse.times(new Matrix(B)).getArray(); // 计算完毕
      for (int j = 0; j < _data.size(); j++) {
        _data.get(j).set(i, (float) A[j][0]); // 更新数据
      }
    }
  }
}
